/*-
 * #%L
 * Fiji distribution of ImageJ for the life sciences.
 * %%
 * Copyright (C) 2010 - 2016 Fiji developers.
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 * #L%
 */

package voltex;

import org.scijava.java3d.Appearance;
import org.scijava.java3d.ColoringAttributes;
import org.scijava.java3d.ImageComponent2D;
import org.scijava.java3d.Material;
import org.scijava.java3d.PolygonAttributes;
import org.scijava.java3d.RenderingAttributes;
import org.scijava.java3d.TexCoordGeneration;
import org.scijava.java3d.Texture;
import org.scijava.java3d.Texture2D;
import org.scijava.java3d.TextureAttributes;
import org.scijava.java3d.TextureUnitState;
import org.scijava.java3d.TransparencyAttributes;
import org.scijava.vecmath.Color3f;
import org.scijava.vecmath.Vector4f;

import ij3d.AxisConstants;
import ij3d.Volume;

/**
 * This class is a helper class whose main task is to create Appearance objects
 * for a specified axis and direction. Each time when ask for an Appearance
 * object, a new Appearance is created. This is necessary, since each slice has
 * another texture. However, all the Appearance objects created by one instance
 * of this class share their Appearance attributes. In this way, it is easy and
 * fast to change color, transparency, etc.
 *
 * @author Benjamin Schmid
 */
public class AppearanceCreator implements AxisConstants {

	/** The volume from which the textures are created */
	private VoltexVolume volume;

	/** Texture mode, e.g. Texture.RGB or so */
	private int textureMode;

	/** Indicates if transparent or opaque texture modes should be used */
	private boolean opaque = false;

	/** TexCoordGeneration object for x direction */
	private TexCoordGeneration xTg;
	/** TexCoordGeneration object for y direction */
	private TexCoordGeneration yTg;
	/** TexCoordGeneration object for z direction */
	private TexCoordGeneration zTg;

	/** texture attributes */
	private TextureAttributes texAttr;
	/** transparency attributes */
	private TransparencyAttributes transAttr;
	/** polygon attributes */
	private PolygonAttributes polyAttr;
	/** material */
	private Material material;
	/** color attributes */
	private ColoringAttributes colAttr;
	/** rendering attributes */
	private RenderingAttributes rendAttr;

	/**
	 * Constructor. Initializes this AppearanceCreator with the given image data.
	 * 
	 * @param volume
	 */
	public AppearanceCreator(final VoltexVolume volume) {
		this(volume, null, 0.1f);
	}

	/**
	 * Initializes this AppearanceCreator with the given image data, color and
	 * transparency values.
	 * 
	 * @param volume
	 * @param color
	 * @param transparency
	 */
	public AppearanceCreator(final VoltexVolume volume, final Color3f color,
		final float transparency)
	{
		initAttributes(color, transparency);
		setVolume(volume);
	}

	/**
	 * Release all stored data.
	 */
	public void release() {
		xTg = null;
		yTg = null;
		zTg = null;
		volume = null;
	}

	/**
	 * Change the image data of this AppearanceCreator
	 * 
	 * @param v
	 */
	public void setVolume(final VoltexVolume v) {
		this.volume = v;
		zTg = new TexCoordGeneration();
		zTg.setPlaneS(new Vector4f(v.xTexGenScale, 0f, 0f,
		// move it to pixel center
			(float) (0.5f * v.pw * v.xTexGenScale)
			// translate it to the origin
				-
				(float) (v.xTexGenScale * v.minCoord.x)));
		zTg.setPlaneT(new Vector4f(0f, v.yTexGenScale, 0f,
			(float) (0.5f * v.ph * v.yTexGenScale) -
				(float) (v.yTexGenScale * v.minCoord.y)));
		yTg = new TexCoordGeneration();
		yTg.setPlaneS(new Vector4f(v.xTexGenScale, 0f, 0f,
			(float) (0.5f * v.pw * v.xTexGenScale) -
				(float) (v.xTexGenScale * v.minCoord.x)));
		yTg.setPlaneT(new Vector4f(0f, 0f, v.zTexGenScale,
			(float) (0.5f * v.pd * v.zTexGenScale) -
				(float) (v.zTexGenScale * v.minCoord.z)));
		xTg = new TexCoordGeneration();
		xTg.setPlaneS(new Vector4f(0f, v.yTexGenScale, 0f,
			(float) (0.5f * v.ph * v.yTexGenScale) -
				(float) (v.yTexGenScale * v.minCoord.y)));
		xTg.setPlaneT(new Vector4f(0f, 0f, v.zTexGenScale,
			(float) (0.5f * v.pd * v.zTexGenScale) -
				(float) (v.zTexGenScale * v.minCoord.z)));
		updateTextureMode();
	}

	/**
	 * Returns whether the created textures are supposed to be opaque.
	 */
	public boolean getOpaqueTextures() {
		return opaque;
	}

	/**
	 * This flag indicates whether textures are opaque or not. This changes
	 * effectively the texture mode, (depending on whether RGB or 8-bit textures
	 * are used) between RGB and RGBA, or LUMINANCE and INTENSITY. This only
	 * effects newly loaded textures. Opaque textures are for example needed for
	 * the orthoslices.
	 */
	public void setOpaqueTextures(final boolean opaque) {
		if (this.opaque != opaque) {
			this.opaque = opaque;
			updateTextureMode();
		}
	}

	/**
	 * Update the texture mode, after the volume has changed.
	 */
	public void updateTextureMode() {
		final boolean rgb = volume.getDataType() == Volume.INT_DATA;

		if (rgb) {
			textureMode = opaque ? Texture.RGB : Texture.RGBA;
		}
		else {
			textureMode = opaque ? Texture.LUMINANCE : Texture.INTENSITY;
		}
	}

	/**
	 * Returns the Appearance object for the specified direction and index. This
	 * is composed of the shared Appearance attributes plus the individual
	 * textures.
	 * 
	 * @param direction
	 * @param index
	 * @return
	 */
	public Appearance getAppearance(final int direction, final int index) {
		final Appearance a = new Appearance();
		a.setCapability(Appearance.ALLOW_TEXTURE_UNIT_STATE_WRITE);
		a.setCapability(Appearance.ALLOW_TEXTURE_WRITE);
		a.setCapability(Appearance.ALLOW_TEXGEN_WRITE);
		a.setMaterial(material);
		a.setTransparencyAttributes(transAttr);
		a.setPolygonAttributes(polyAttr);
		a.setColoringAttributes(colAttr);
		a.setRenderingAttributes(rendAttr);

		final TextureUnitState[] tus = new TextureUnitState[2];
		tus[0] =
			new TextureUnitState(getTexture(direction, index, volume), texAttr,
				getTg(direction));
		tus[0].setCapability(TextureUnitState.ALLOW_STATE_WRITE);
		tus[1] = null;
		a.setTextureUnitState(tus);
		return a;
	}

	/**
	 * Set the transparency for all the textures that were loaded by this
	 * AppearanceCreator.
	 * 
	 * @param f
	 */
	public void setTransparency(final float f) {
		transAttr.setTransparency(f);
	}

	/**
	 * Set the threshold for all the textures that were loaded by this
	 * AppearanceCreator. Pixel values below the threshold are not rendered.
	 * 
	 * @param f
	 */
	public void setThreshold(final float f) {
		rendAttr.setAlphaTestValue(f);
	}

	/**
	 * Set the color for all the textures that were loaded by this
	 * AppearanceCreator. Pixel values below the threshold are not rendered.
	 * 
	 * @param c
	 */
	public void setColor(final Color3f c) {
		colAttr.setColor(c);
	}

	/**
	 * Returns the texture for the specified axis and slice
	 * 
	 * @param axis
	 * @param index
	 * @return
	 */
	public Texture2D getTexture(final int axis, final int index) {
		return getTexture(axis, index, volume);
	}

	/**
	 * Returns the texture for the specified axis and slice
	 * 
	 * @param axis
	 * @param index
	 * @param vol
	 * @return
	 */
	public Texture2D getTexture(final int axis, final int index,
		final VoltexVolume vol)
	{
		int sSize = 0, tSize = 0;
		ImageComponent2D pArray = null;
		switch (axis) {
			case Z_AXIS:
				sSize = vol.xTexSize;
				tSize = vol.yTexSize;
				pArray = vol.getImageComponentZ(index);
				break;
			case Y_AXIS:
				sSize = vol.xTexSize;
				tSize = vol.zTexSize;
				pArray = vol.getImageComponentY(index);
				break;
			case X_AXIS:
				sSize = vol.yTexSize;
				tSize = vol.zTexSize;
				pArray = vol.getImageComponentX(index);
				break;
		}
		final Texture2D tex =
			new Texture2D(Texture.BASE_LEVEL, textureMode, sSize, tSize);

		tex.setImage(0, pArray);
		tex.setEnable(true);
		tex.setMinFilter(Texture.BASE_LEVEL_LINEAR);
		tex.setMagFilter(Texture.BASE_LEVEL_LINEAR);

		tex.setBoundaryModeS(Texture.CLAMP);
		tex.setBoundaryModeT(Texture.CLAMP);
		return tex;
	}

	/**
	 * Get the TextureGeneration of the specified direction.
	 * 
	 * @param direction
	 * @return
	 */
	public TexCoordGeneration getTg(final int direction) {
		switch (direction) {
			case X_AXIS:
				return xTg;
			case Y_AXIS:
				return yTg;
			case Z_AXIS:
				return zTg;
		}
		return null;
	}

	/**
	 * Initialize the Appearance attributes.
	 * 
	 * @param color
	 * @param transparency
	 */
	private void initAttributes(final Color3f color, final float transparency) {

		texAttr = new TextureAttributes();
		texAttr.setTextureMode(TextureAttributes.MODULATE);
		texAttr.setPerspectiveCorrectionMode(TextureAttributes.NICEST);

		transAttr = new TransparencyAttributes();
		transAttr.setCapability(TransparencyAttributes.ALLOW_VALUE_WRITE);
		transAttr.setTransparencyMode(TransparencyAttributes.BLENDED);
		transAttr.setSrcBlendFunction(TransparencyAttributes.BLEND_SRC_ALPHA);
		transAttr
			.setDstBlendFunction(TransparencyAttributes.BLEND_ONE_MINUS_SRC_ALPHA);
		transAttr.setTransparency(transparency);

		polyAttr = new PolygonAttributes();
		polyAttr.setCullFace(PolygonAttributes.CULL_NONE);

		material = new Material();
		material.setLightingEnable(true);

		colAttr = new ColoringAttributes();
		colAttr.setCapability(ColoringAttributes.ALLOW_COLOR_WRITE);
		colAttr.setShadeModel(ColoringAttributes.NICEST);
		if (color == null) {
			colAttr.setColor(1f, 1f, 1f);
		}
		else {
			colAttr.setColor(color);
		}

		// Avoid rendering of voxels having an alpha value of zero
		rendAttr = new RenderingAttributes();
		rendAttr.setCapability(RenderingAttributes.ALLOW_ALPHA_TEST_VALUE_WRITE);
		rendAttr.setAlphaTestValue(0.1f);
		rendAttr.setAlphaTestFunction(RenderingAttributes.GREATER);
	}
}
